<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body style="margin: 0; padding: 0;"></body>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script type="text/javascript">
    // 连接的对象类型
    var Connection = {
      createNew: function () {
        var connection = {}
        connection.startPoint = null
        connection.endPoint = null
        connection.linePoints = []
        return connection
      },
    }
    let myConn = []
    let origin = []
    // SVG尺寸
    var w = 1920
    var h = 1075
    const leftX = -10
    const rightX = 300

    // 数据
    var dataSet = [
      { x: 10, y: 50, width: 20, height: 20, name: 'A1' },
      { x: 60, y: 50, width: 20, height: 20, name: 'A2' },
      { x: 110, y: 50, width: 20, height: 20, name: 'A3' },
      { x: 160, y: 50, width: 20, height: 20, name: 'A4' },
      { x: 210, y: 50, width: 20, height: 20, name: 'A5' },
      { x: 260, y: 50, width: 20, height: 20, name: 'A6' },
      { x: 10, y: 100, width: 20, height: 20, name: 'A7' },
      { x: 60, y: 100, width: 20, height: 20, name: 'A8' },
      { x: 110, y: 100, width: 20, height: 20, name: 'A9' },
      { x: 160, y: 100, width: 20, height: 20, name: 'A10' },
      { x: 210, y: 100, width: 20, height: 20, name: 'A11' },
      { x: 260, y: 100, width: 20, height: 20, name: 'A12' },
    ]
    var forbiddenArea = [{ x: 0, y: 40, width: 290, height: 90 }]

    // 创建SVG容器
    var svg = d3
      .select('body')
      .append('svg')
      // .attr('width', w).attr('height', h)
      .attr('viewBox', [0, 0, w, h])
    const g = svg.append('g').attr('cursor', 'grab')
    g.selectAll('.forbidden')
      .data(forbiddenArea)
      .enter()
      .append('rect')
      .attr('x', (d) => d.x)
      .attr('y', (d) => d.y)
      .attr('width', (d) => d.width)
      .attr('height', (d) => d.height)
      .attr('rx', 5)
      .attr('ry', 5)
      .attr('class', 'fobidden')
      .attr('fill', 'none')
      .attr('stroke-dasharray', '10 5')
      .attr('stroke-width', '1px')
      .attr('stroke', 'gray')

    // 创建矩形
    var rects = g.selectAll('.point').data(dataSet).enter().append('rect')

    // 根据数据设置每个圆的属性
    rects
      .attr('x', (d) => d.x)
      .attr('y', (d) => d.y)
      .attr('width', (d) => d.width)
      .attr('height', (d) => d.height)
      .attr('fill', 'green')
      .attr('class', 'point')
      .attr('id', (d) => d.name)
      .on('mouseover', function () {
        d3.select(this).transition().duration(200).attr('fill', 'red')
      })
      .on('mouseout', function () {
        d3.select(this)
          .transition()
          .delay(200)
          .duration(200)
          .attr('fill', 'green')
      })

    // 添加文本
    // g.selectAll('text')
    //   .data(dataSet)
    //   .enter()
    //   .append('text')
    //   .text((d) => d.name)
    //   .attr('x', (d) => d.x + (d.width - 18) / 2)
    //   .attr('y', (d) => d.y + 15)
    //   .attr('font-size', `14px`)
    //   .attr('fill', 'white')

    rects.on('dblclick', function () {
      origin.push(d3.select(this))
      if (origin.length == 2) {
        handlePoints()
        origin = []
      }
    })

    // 缩放平移设置
    svg
      .call(
        d3
          .zoom()
          .extent([
            [0, 0],
            [w, h],
          ])
          .scaleExtent([0.1, 10])
          .on('zoom', zoomed)
      )
      // 关闭双击缩放
      .on('dblclick.zoom', null)

    function zoomed() {
      g.attr('transform', d3.event.transform)
    }

    function handlePoints() {
      if (origin.length == 0) return
      const tmp = Connection.createNew()
      tmp.startPoint = {
        x: Number(origin[0].attr('x')),
        y: Number(origin[0].attr('y')),
        width: Number(origin[0].attr('width')),
        height: Number(origin[0].attr('height')),
        name: origin[0].attr('id'),
      }
      tmp.endPoint = {
        x: Number(origin[1].attr('x')),
        y: Number(origin[1].attr('y')),
        width: Number(origin[1].attr('width')),
        height: Number(origin[1].attr('height')),
        name: origin[1].attr('id'),
      }
      if (
        tmp.startPoint.x == tmp.endPoint.x &&
        tmp.startPoint.y == tmp.endPoint.y
      ) {
        console.log('起始点和结束点是同一个点，取消连线操作')
        return
      }
      // tmp.linePoints.push({ x: tmp.startPoint.x, y: tmp.startPoint.y })
      // tmp.linePoints.push({ x: tmp.endPoint.x, y: tmp.endPoint.y })
      calculateLinePoints(tmp)
      adjustLinePoints(tmp)
      myConn.push(tmp)
      console.log(myConn)
      g.append('polyline')
        .attr('points', getPolylineExp(tmp))
        .attr('stroke', 'blue')
        .attr('stroke-width', '1px')
        .attr('fill', 'none')
        .on('mouseover', function () {
          d3.select(this).transition().duration(200).attr('stroke', 'orange')
        })
        .on('mouseout', function () {
          d3.select(this)
            .transition()
            .delay(200)
            .duration(200)
            .attr('stroke', 'blue')
        })
    }
    // 获取曲线的polyline的表达式
    function getPolylineExp(connection) {
      let result = ''
      for (let i = 0; i < connection.linePoints.length; i++) {
        result += `${connection.linePoints[i].x},${connection.linePoints[i].y} `
      }
      return result
    }

    // 计算两个连接点的曲线点集合
    function calculateLinePoints(connection) {
      const start = {
        x: connection.startPoint.x,
        y: connection.startPoint.y,
        width: connection.startPoint.width,
        height: connection.startPoint.height,
      }
      const end = {
        x: connection.endPoint.x,
        y: connection.endPoint.y,
        width: connection.endPoint.width,
        height: connection.endPoint.height,
      }
      const startNext = originUpOrDown(start)
      const endNext = originUpOrDown(end)
      // 判断startNext和endNext能否直接连接
      if (canLineDirect(startNext, endNext)) {
        connection.linePoints.push(start)
        connection.linePoints.push(startNext)
        connection.linePoints.push(endNext)
        connection.linePoints.push(end)
      } else {
        let point1 = { x: leftX, y: startNext.y }
        let point2 = { x: leftX, y: endNext.y }
        if (
          startNext.x - leftX + endNext.x - leftX <
          rightX - startNext.x + rightX - endNext.x
        ) {
          point1.x = leftX
          point2.x = leftX
        } else {
          point1.x = rightX
          point2.x = rightX
        }
        connection.linePoints.push(start)
        connection.linePoints.push(startNext)
        connection.linePoints.push(point1)
        connection.linePoints.push(point2)
        connection.linePoints.push(endNext)
        connection.linePoints.push(end)
      }
    }
    // 调整两个连接点的曲线点集合
    function adjustLinePoints(connection) {
      const dx = 10
      const startPoint = connection.linePoints[0]
      const endPoint = connection.linePoints[connection.linePoints.length - 1]
      for (
        let i = 0, length = connection.linePoints.length;
        i < length - 1;
        i++
      ) {
        const point1 = connection.linePoints[i]
        const point2 = connection.linePoints[i + 1]
        for (let j = 0; j < myConn.length; j++) {
          const linePoints = myConn[j].linePoints
          const isAdjust = adjustOneLinePoint(
            linePoints,
            point1,
            point2,
            startPoint,
            endPoint
          )
          if (isAdjust) {
            i = -1
            break
          }
        }
      }
    }
    // 根据一条线和两个点进行调整
    function adjustOneLinePoint(
      linePoints,
      point1,
      point2,
      startPoint,
      endPoint
    ) {
      const ddx = 3
      const target = [point1, point2]
      if (point1.x == point2.x) {
        // 过滤出X相同的点
        let xLine = linePoints.filter((item) => item.x == point1.x)
        if (xLine.length == 0) return
        // 查出来的点必须是成对出现的
        if (xLine.length % 2 != 0) throw new Error('存在多个点，错误')
        else {
          // 对上下连接时，特殊情况的处理
          if (isEqual(point1, startPoint)) xLine = [xLine[0], xLine[1]]
          else if (isEqual(point2, endPoint))
            xLine = [xLine[xLine.length - 2], xLine[xLine.length - 1]]
        }
        // 升序排列
        xLine.sort((a, b) => a.y - b.y)
        target.sort((a, b) => a.y - b.y)
        // 判断y轴方向有没有重叠部分，有重叠需要做偏移
        if (!(xLine[1].y < target[0].y || target[1].y < xLine[0].y)) {
          let dir = 1
          if (!isEqual(point1, startPoint) && !isEqual(point2, endPoint)) {
            dir = calXDirection(target[0], startPoint, endPoint)
          }
          point1.x = point1.x + dir * ddx
          point2.x = point2.x + dir * ddx
          return true
        }
      } else if (point1.y == point2.y) {
        // 过滤出Y相同的点
        const yLine = linePoints.filter((item) => item.y == point1.y)
        if (yLine.length == 0) return
        if (yLine.length != 2) throw new Error('存在多个点，错误')
        // 升序排列
        yLine.sort((a, b) => a.x - b.x)
        target.sort((a, b) => a.x - b.x)
        // 判断X轴方向有没有重叠部分，有重叠需要做偏移
        if (!(yLine[1].x < target[0].x || target[1].x < yLine[0].x)) {
          const dir = calYDirection(target[0], startPoint, endPoint)
          point1.y = point1.y + dir * ddx
          point2.y = point2.y + dir * ddx
          return true
        }
      } else {
        throw new Error('两个点不在同一行或同一列中，错误')
      }
      return false
    }
    // 原始点的下一个点是朝上还是朝下，同时调整目标点的
    function originUpOrDown(point) {
      const up = Math.abs(forbiddenArea[0].y - point.y)
      const down = Math.abs(
        forbiddenArea[0].y + forbiddenArea[0].height - point.y
      )
      if (up < down) {
        return { x: point.x, y: forbiddenArea[0].y - 10 }
      } else {
        point.y += point.height
        return {
          x: point.x,
          y: forbiddenArea[0].y + forbiddenArea[0].height + 10,
        }
      }
    }
    // 两个点能否不经过禁止区域直接连接
    function canLineDirect(point1, point2) {
      return (
        (point1.x < forbiddenArea[0].x && point2.x < forbiddenArea[0].x) ||
        (point1.x > forbiddenArea[0].x + forbiddenArea[0].width &&
          point2.x > forbiddenArea[0].x + forbiddenArea[0].width) ||
        (point1.y < forbiddenArea[0].y && point2.y < forbiddenArea[0].y) ||
        (point1.y > forbiddenArea[0].y + forbiddenArea[0].height &&
          point2.y > forbiddenArea[0].y + forbiddenArea[0].height)
      )
    }
    // 计算偏差Y轴上的运算方向
    function calYDirection(point1, startPoint, endPoint) {
      const dis1 = calDistance(point1, startPoint)
      const dis2 = calDistance(point1, endPoint)
      if (dis1 <= dis2) {
        return point1.y >= startPoint.y ? 1 : -1
      } else {
        return point1.y >= endPoint.y ? 1 : -1
      }
    }
    // 计算偏差X轴上的运算方向
    function calXDirection(point1, startPoint, endPoint) {
      const dis1 = calDistance(point1, startPoint)
      const dis2 = calDistance(point1, endPoint)
      if (dis1 <= dis2) {
        return point1.x >= startPoint.x ? 1 : -1
      } else {
        return point1.x >= endPoint.x ? 1 : -1
      }
    }
    // 计算两个点的距离
    function calDistance(point1, point2) {
      return Math.sqrt(
        (point1.x - point2.x) * (point1.x - point2.x) +
          (point1.y - point2.y) * (point1.y - point2.y)
      )
    }
    function isEqual(point1, point2) {
      return point1.x == point2.x && point1.y == point2.y
    }
  </script>
</html>
